SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
4.8 KB · 101 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { publicProfile } from '@/lib/account/queries';5import { Card, CardHeader, Delta, Table, th, td, tdNum } from '@/components/ui/primitives';6import { Donut } from '@/components/account/charts';7import { ValueSourceBadge } from '@/components/account/portfolio-widgets';8import { fmtMoney, confidenceLabel } from '@/lib/format';910export async function generateMetadata({ params }: { params: Promise<{ handle: string; slug: string }> }): Promise<Metadata> {11  const { handle, slug } = await params;12  const p = await publicProfile(handle);13  const c = p?.collections.find((x) => x.collection.publicSlug === slug);14  if (!p || !c) return { title: 'Collection not found' };15  return { title: `${c.collection.name} · ${p.user.name ?? `@${handle}`}`, description: c.collection.description ?? `${c.summary.itemCount} items on RareIndex` };16}1718export default async function PublicCollectionPage({ params }: { params: Promise<{ handle: string; slug: string }> }) {19  const { handle, slug } = await params;20  const p = await publicProfile(handle);21  const c = p?.collections.find((x) => x.collection.publicSlug === slug);22  if (!p || !c) notFound();23  const items = p.items.filter((i) => i.collectionId === c.collection.id);24  const rows = [...c.summary.items].sort((a, b) => (b.valueUsd ?? 0) - (a.valueUsd ?? 0));25  return (26    <div className="mx-auto max-w-5xl">27      <nav className="mb-2 pt-4 text-xs text-muted">28        <Link href={`/u/${handle}`} className="hover:text-fg">29          @{handle}30        </Link>{' '}31        / <span className="text-fg">{c.collection.name}</span>32      </nav>33      <header className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">34        <div>35          <h1 className="text-2xl font-semibold tracking-tight">{c.collection.name}</h1>36          {c.collection.description ? <p className="mt-1 text-sm text-muted">{c.collection.description}</p> : null}37        </div>38        <div className="text-right">39          <p className="num text-2xl font-semibold">{c.summary.valuedCount ? fmtMoney(c.summary.valueUsd) : '—'}</p>40          <p className="text-xs text-muted">41            {c.summary.itemCount} items · confidence {confidenceLabel(c.summary.confidence)}42          </p>43        </div>44      </header>45      <div className="mb-5 grid gap-4 md:grid-cols-3">46        <Card className="md:col-span-1">47          <CardHeader title="Allocation" />48          <div className="p-4">49            <Donut data={c.summary.allocationByCategory.map((b) => ({ label: b.label, value: b.valueUsd }))} size={100} />50          </div>51        </Card>52        <Card className="md:col-span-2">53          <CardHeader title="Items" />54          <Table>55            <thead>56              <tr>57                <th className={th}>Item</th>58                <th className={th}>Variant</th>59                <th className={`${th} text-right`}>Qty</th>60                <th className={`${th} text-right`}>Value</th>61                <th className={th}>Basis</th>62              </tr>63            </thead>64            <tbody>65              {rows.map((i) => {66                const src = items.find((x) => x.id === i.id)!;67                return (68                  <tr key={i.id}>69                    <td className={td}>70                      <div className="flex items-center gap-2.5">71                        {src.photos[0] || src.heroImageUrl ? (72                          // eslint-disable-next-line @next/next/no-img-element73                          <img src={src.photos[0] ?? src.heroImageUrl ?? ''} alt="" className="h-9 w-9 rounded-sm object-cover" />74                        ) : (75                          <span className="h-9 w-9 rounded-sm bg-inset" />76                        )}77                        <Link href={`/asset/${src.assetSlug}`} className="max-w-[260px] truncate font-medium hover:underline">78                          {i.title}79                        </Link>80                      </div>81                    </td>82                    <td className={`${td} text-xs`}>{src.variantLabel ?? (i.grader ? `${i.grader.toUpperCase()} ${i.grade ?? ''}` : src.condition ?? '—')}</td>83                    <td className={tdNum}>{i.quantity}</td>84                    <td className={tdNum}>{i.valueUsd === null ? <span className="text-subtle">—</span> : fmtMoney(i.valueUsd)}</td>85                    <td className={td}>86                      <ValueSourceBadge item={i} />87                    </td>88                  </tr>89                );90              })}91            </tbody>92          </Table>93        </Card>94      </div>95      <p className="text-[11px] text-subtle">96        Shared by @{handle}. Values are RareIndex Valuation estimates; <Delta value={c.summary.returnPct} /> return is shown without purchase details. RareIndex does not authenticate items.97      </p>98    </div>99  );100}101